/*
 * ModelCC, distributed under ModelCC Shared Software License, www.modelcc.org
 */

package org.modelcc.language.syntax.builder;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import org.modelcc.io.java.Reflection;
import org.modelcc.language.factory.SymbolIdentifier;
import org.modelcc.language.factory.ListContent;
import org.modelcc.language.metamodel.LanguageElement;
import org.modelcc.language.metamodel.MemberCollection;
import org.modelcc.language.metamodel.CompositeLanguageElement;
import org.modelcc.language.metamodel.LanguageModel;
import org.modelcc.language.metamodel.LanguageMember;
import org.modelcc.language.syntax.ObjectWrapper;
import org.modelcc.language.syntax.ParserMetadata;
import org.modelcc.language.syntax.SymbolBuilder;
import org.modelcc.language.syntax.RuleSymbol;
import org.modelcc.language.syntax.Symbol;

/**
 * Reference symbol builder
 * 
 * @author Luis Quesada (lquesada@modelcc.org) & Fernando Berzal (fberzal@modelcc.org)
 */
public final class ReferenceSymbolPostBuilder extends SymbolBuilder implements Serializable 
{
    /**
     * Constructor
     */
    public ReferenceSymbolPostBuilder(LanguageModel model) 
    {
    	super(model);
    }
    
    /**
     * Build a symbol
     * @param t symbol to be built.
     * @param data the parser metadata.
     * @return true if the symbol is valid, false if not
     */
    @Override
    public boolean build (Symbol t, ParserMetadata data) 
    {
    	if (!data.getLazyReferences().contains(t))
    		return true;

    	SymbolIdentifier eid = (SymbolIdentifier)t.getType();
    	CompositeLanguageElement ce = (CompositeLanguageElement) eid.getElement();
    	Class c = ce.getElementClass();
    	Object o;
    	boolean valid = true;

    	try {
    		o = c.newInstance();
    		Set<RuleSymbol> proc = new HashSet<RuleSymbol>();
    		Set<Field> filled = new HashSet<Field>();
    		for (int i = 0;i < t.size();i++) {
    			Symbol s = t.getContent(i);
    			RuleSymbol re = t.getElement(i);
    			proc.add(re);
    			if (re.getLanguageMember()!=null) {
    				LanguageMember ct = re.getLanguageMember();
    				Field fld = Reflection.findField(c,ct.getID());
    				filled.add(fld);
    				Object list;
    				Object[] listData;
    				Method addm;

    				if (fld != null) {
    					fld.setAccessible(true);
    					if (!ct.getClass().equals(MemberCollection.class)) {
    						fld.set(o,s.getUserData());
    					} else {
    						MemberCollection mc = (MemberCollection)ct;
    						ListContent listContents = (ListContent) s.getUserData();
    						listData = listContents.getContent();
    						Symbol extra = listContents.getSymbol();
    						RuleSymbol extraRe = listContents.getRuleSymbol();
    						if (extraRe != null) {
    							LanguageMember extraCt = extraRe.getLanguageMember();
    							Field extraFld = Reflection.findField(c,extraCt.getID());
    							filled.add(extraFld);
    							if (extraFld != null) {
    								extraFld.setAccessible(true);
    								extraFld.set(o,extra.getUserData());
    							}
    						}
    						if (listData.length<mc.getMinimumMultiplicity())
    							valid = false;
    						if (mc.getMaximumMultiplicity() != -1) {
    							if (listData.length>mc.getMaximumMultiplicity())
    								valid = false;
    						}
    						switch (mc.getCollection()) {
    						case LIST:
    							if (fld.getType().isInterface())
    								list = ArrayList.class.newInstance();
    							else
    								list = fld.getType().newInstance();
    							addm = list.getClass().getDeclaredMethod("add",Object.class);
    							if (listData != null)
    								for (int j=0; j<listData.length; j++)
    									addm.invoke(list,listData[j]);
    							fld.set(o,list);
    							break;
    						case ARRAY:
    							list = Array.newInstance(ct.getElementClass(),listData.length);
    							if (listData != null)
    								for (int j=0; j<listData.length; j++)
    									Array.set(list, j, listData[j]);
    							fld.set(o,list);
    							break;
    						case SET:
    							if (fld.getType().isInterface())
    								list = HashSet.class.newInstance();
    							else
    								list = fld.getType().newInstance();
    							addm = list.getClass().getDeclaredMethod("add",Object.class);
    							if (listData != null)
    								for (int j=0; j<listData.length;j++)
    									addm.invoke(list,listData[j]);
    							fld.set(o,list);
    							break;
    						}
    					}
    				}
    			}
    		}

    		if (ce.getKeyMembers().isEmpty())
    			valid = false;

    		if (valid) {
    			Map<ObjectWrapper,Object> idmap = data.getKeys(c);
    			ObjectWrapper kw = ObjectWrapper.createKeyWrapper(o, getModel(), data.getMap());

    			if (idmap.containsKey(kw)) {
    				t.setUserData(idmap.get(kw));
    				if (data.getUsed().get(t) != null) {
    					for (Symbol sym: data.getUsed().get(t)) {
    						propagateChanges(sym,t,data.getUsed(),0);
    					}
    				}
    			} else {
    				valid = false;
    			}
    		}
    	} catch (Exception ex) {
    		log(Level.SEVERE, null, ex);
    		return false;
    	}
    	return valid;
    }

    private void propagateChanges(Symbol t, Symbol updated, Map<Symbol, Set<Symbol>> usedIn,int array) 
    		throws IllegalArgumentException, IllegalAccessException, SecurityException, NoSuchFieldException 
    {
        if (t.equals(updated))
            return;
        Object tData = t.getUserData();
        if (tData.getClass().equals(ListContent.class)) {
        	tData = ((ListContent)tData).getContent();
        }
        Object uData = updated.getUserData();
        if (uData.getClass().equals(ListContent.class)) {
        	uData = ((ListContent)uData).getContent();
        }

        if (((SymbolIdentifier)(t.getRule().getLeft().getType())).getElement().getClass().equals(LanguageElement.class)) {
            t.setUserData(uData);
            if (usedIn.get(t) != null) {
                for (Iterator<Symbol> ite = usedIn.get(t).iterator();ite.hasNext();) {
                    propagateChanges(ite.next(),t,usedIn,0);
                }
            }
            return;
        }

        if (tData.getClass().isArray() && !uData.getClass().isArray()) {
        	
            Object[] atData = (Object[])tData;
            if (!uData.equals(atData[0])) {
                atData[0] = uData;
                if (usedIn.get(t) != null) {
                    for (Iterator<Symbol> ite = usedIn.get(t).iterator();ite.hasNext();) {
                        propagateChanges(ite.next(),t,usedIn,array+1);
                    }
                }
            }
            
        } else if (tData.getClass().isArray() && uData.getClass().isArray()) {
        	
            Object[] atData = (Object[])tData;
            Object[] auData = (Object[])uData;

            if (atData.length>array) {
	            if (!auData[0].equals(atData[array])) {
	                atData[array] = auData[array-1];
	                if (usedIn.get(t) != null) {
	                    for (Iterator<Symbol> ite = usedIn.get(t).iterator();ite.hasNext();) {
	                        propagateChanges(ite.next(),t,usedIn,array+1);
	                    }
	                }
	            }
            } else {
                if (usedIn.get(t) != null) {
                    for (Iterator<Symbol> ite = usedIn.get(t).iterator();ite.hasNext();) {
                        propagateChanges(ite.next(),t,usedIn,array);
                    }
                }
            }

        } else if (!tData.getClass().isArray() && uData.getClass().isArray()) {
        
        	int index = t.getContents().indexOf(updated);
            RuleSymbol re = t.getElement(index);
            LanguageMember ct = re.getLanguageMember();
            Field fld = Reflection.findField(tData.getClass(),ct.getID());
            Object content = fld.get(tData);
            Object[] auData = (Object[])uData;
            Object[] aContent = (Object[])content;
            if (!auData[array-1].equals(aContent[array-1])) {
                aContent[array-1] = auData[array-1];
                for (int i = 0;i < aContent.length;i++) {
                    if (usedIn.get(t) != null) {
                        for (Iterator<Symbol> ite = usedIn.get(t).iterator();ite.hasNext();) {
                            propagateChanges(ite.next(),t,usedIn,0);
                        }
                    }
                }
            }
            
        } else { // tData is not array and uData is not array
        	
            int index = t.getContents().indexOf(updated);
            RuleSymbol re = t.getElement(index);
            LanguageMember ct = re.getLanguageMember();
            Field fld = Reflection.findField(tData.getClass(),ct.getID());
            fld.setAccessible(true);
            Object content = fld.get(tData);
            if (!uData.equals(content)) {
                fld.set(tData,uData);
                if (usedIn.get(t) != null) {
                    for (Iterator<Symbol> ite = usedIn.get(t).iterator();ite.hasNext();) {
                        propagateChanges(ite.next(),t,usedIn,0);
                    }
                }
            }
        }
    }
}
